﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using TA.DotNetUtils;
using System.Text.RegularExpressions;

namespace TA.Turbo
{
	/// <summary>
	/// Obfuscates and compresses JavaScript.
	/// </summary>
	public class JSObfuscator
	{
		#region PUBLIC
		#region METHODS
		/// <summary>
		/// Performs the obfuscation.
		/// </summary>
		/// <param name="js"></param>
		/// <param name="optimize"></param>
		/// <returns></returns>
		public string Obfuscate(string js, bool optimize)
		{
			Context c = new Context(js);
			StringBuilder sb = new StringBuilder();
			JSToken lastToken = null;

			while (true)
			{
				JSToken token = this.GetNext(c, 0);
				if (token == null)
					break;

				switch (token.Type)
				{
					case JSTokenType.Comment:
					case JSTokenType.WhiteSpace:
						this.Obfuscate_CommentWhiteSpace(c, optimize, sb, token, lastToken);
						break;
					case JSTokenType.BuiltinKeyword:
						this.Obfuscate_BuiltinKeyword(c, optimize, sb, token);
						break;
					case JSTokenType.BuiltinSymbol:
						this.Obfuscate_BuiltinSymbol(c, optimize, sb, token);
						break;
					case JSTokenType.Literal:
						this.Obfuscate_Literal(c, optimize, sb, token);
						break;
					case JSTokenType.Identifier:
						this.Obfuscate_Identifier(c, optimize, sb, token);
						break;
				}

				lastToken = token;
			}

			return sb.ToString();
		}
		/// <summary>
		/// Translates an identifier into its obfuscated counter-part.
		/// </summary>
		/// <remarks>
		/// This overload will not obfuscate the ID if it hasn't been obfuscated already.
		/// </remarks>
		/// <param name="identifier"></param>
		/// <returns></returns>
		public string TranslateId(string identifier)
		{
			return this.TranslateId(identifier, false);
		}
		#endregion
		#endregion
		#region INTERNAL
		#region DECLARATION
		enum JSTokenType
		{
			None,
			WhiteSpace,
			Comment,
			BuiltinKeyword,
			BuiltinSymbol,
			Literal,
			Identifier,
		}
		class JSToken
		{
			internal JSToken(JSTokenType type, string text)
			{
				this.Type = type;
				this.Text = text;
			}
			internal JSTokenType Type;
			internal string Text;
			public override string ToString()
			{
				return this.Type.ToString() + ", '" + this.Text + "'";
			}
			public override bool Equals(object that)
			{
				if (that is JSToken)
					return this.Equals((JSToken)that);
				return false;
			}
			public override int GetHashCode()
			{
				return this.Text.GetHashCode();
			}
			public bool Equals(JSToken that)
			{
				return
					(this.Type == JSTokenType.None || that.Type == JSTokenType.None || this.Type == that.Type) &&
					(this.Text == null || that.Text == null || this.Text == that.Text);
			}
		}
		struct Context
		{
			//internal Context()
			//{
			//}
			internal Context(string script)
			{
				this.Reader = new StringReader(script);
				this.Queue = new List<JSToken>();
			}

			internal StringReader Reader;
			internal List<JSToken> Queue;
		}
		#endregion
		#region CONST/STATIC
		#region KEYWORDS
		static readonly string[] Keywords =
			new[] { 
				"new", "function", "arguments", "var", "prototype", "constructor", "typeof", "instanceof", "this", "void",
				"if", "else", "for", "in", "while", "do", "switch", "case", "default", "break", "continue", "return", 
				"try", "catch", "finally", "throw", 
				"with",
				"null", "undefined", "true", "false", "delete", "debugger", "Infinity", "NaN",

				"parseInt", "parseFloat", "isFinite", "isNaN",
				"alert", "confirm", "print", "prompt",
				"moveBy", "moveTo", "resizeBy", "resizeTo", "scroll", "scrollBy", "scrollTo", 
				"setInterval", "clearInterval", "setTimeout", "clearTimeout", 
				"decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape", "unescape", "eval",
		
				"document", 
				"alinkColor", "anchors", "applets", "bgColor", "body", "characterSet", "compatMode", "cookie", "defaultView", "designMode", "doctype", "documentElement", "domain", "embeds", "fgColor", "firstChild", "forms", "height", "images", "implementation", "lastModified", "linkColor", "links", "location", "namespaceURI", "plugins", "referrer", "styleSheets", "title", "URL", "vlinkColor", "width", "clear", "close", "createAttribute", "createDocumentFragment", "createElement", "createElementNS", "createTextNode", "createEvent", "createNSResolver", "createRange", "execCommand", "evaluate", "getElementById", "getElementsByName", "getElementsByTagName", "importNode", "loadOverlay", "open", "queryCommandEnabled", "queryCommandIndeterm", "queryCommandState", "queryCommandValue", "write", "writeln",

				"history", 
				"current", "next", "previous", "back", "forward", "go",

				"location", 
				"hash", "host", "hostname", "href", "pathname", "port", "protocol", "search", 				
				
				"window", "contentWindow", "contentDocument", 
				"Components", "content", "closed", "controllers", "crypto", "defaultStatus", "directories", "document", "frameElement", "frames", "history", "innerHeight", "innerWidth", "length", "location", "locationbar", "menubar", "name", "navigator", "opener", "outerHeight", "outerWidth", "pageXOffset", "pageYOffset", "parent", "personalbar", "pkcs11", 
				"screenX", "screenY", "scrollbars", "scrollMaxX", 
				"scrollMaxY", "scrollX", "scrollY", "self", "sidebar", "status", "statusbar", "toolbar", "top", "window",
				"alert", "atob", "back", "btoa", "captureEvents", "clearInterval", "clearTimeout", "close", "confirm", "dump", "escape", "find", "focus", "forward", "getAttention", "getComputedStyle", "getSelection", "home", "moveBy", "moveTo", "open", "openDialog", "print", "prompt", "releaseEvents", "resizeBy", "resizeTo", "scroll", "scrollBy", "scrollByLines", "scrollByPages", "scrollTo", "setInterval", "setTimeout", "sizeToContent", "stop", "unescape", "updateCommands",
				"onabort", "onblur", "onchange", "onclick", "onclose", "ondragdrop", "onerror", "onfocus", "onkeydown", "onkeypress", "onkeyup", "onload", "onmousedown", "onmousemove", "onmouseout", "onmouseover", "onmouseup", "onpaint", "onreset", "onresize", "onscroll", "onselect", "onsubmit", "onunload", "onhashchange", 

				"event",
				"originalTarget", "target", "currentTarget", 
				"srcElement", "fromElement", "toElement",
				"cancelable", "timeStamp", "bubbles", "eventPhase", "cancelBubble", "stopPropagation", "returnValue", "preventDefault",
				"which", "rangeParent", "rangeOffset", "isChar", "ctrlKey", "shiftKey", "keyCode", "metaKey", "charCode", "altKey", "view", "detail",
				"pageX", "pageY", "clientX", "clientY", "layerX", "layerY",

				"screen", 
				"availTop", "availLeft", "availHeight", "availWidth", "colorDepth", "height", "left", "pixelDepth", "top",  "width", 			

				"navigator",
				"userAgent", "appVersion", "appName",
				
				"Object", 
				"toString", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "valueOf",
				
				"Error", 
				"ignoreCase", "lastIndex", "message", "multiline", "name",				

				"Date", 
				"getDate", "getDay", "getFullYear", "getHours", "getMilliseconds", "getMinutes", "getMonth", "getSeconds", "getTime", "getTimezoneOffset", "getYear", 
				"getUTCDate", "getUTCDay", "getUTCFullYear", "getUTCHours", "getUTCMilliseconds", "getUTCMinutes", "getUTCMonth", "getUTCSeconds", 				
				"setDate", "setFullYear", "setHours", "setMilliseconds", "setMinutes", "setMonth", "setSeconds", "setTime", "setYear", 
				"setUTCDate", "setUTCFullYear", "setUTCHours", "setUTCMilliseconds", "setUTCMinutes", "setUTCMonth", "setUTCSeconds", 
				"toGMTString", "toLocaleString", "toLocaleDateString", "toLocaleTimeString", "toSource", "toUTCString", 
				
				"String", 
				"charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "match", "replace", "search", "slice", "split", "substr", "substring", "toLowerCase", "toSource", "toString", "toUpperCase", "fromCharCode",
				
				"Number", 
				"toExponential", "toFixed", "toPrecision",

				"Function", 
				"apply", "call",
				
				"Math", 
				"E", "LN2", "LN10", "LOG2E", "LOG10E", "PI", "SQRT1_2", "SQRT2", "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "exp", "floor", "log", "max", "min", "pow", "random", "round", "sin", "sqrt", "tan", 
				
				
				"RegExp", 
				"global", "ignoreCase", "lastIndex", "source", "exec", "test", "toSource", "index", 			
				
				"XMLHttpRequest",
				"open", "setRequestHeader", "send", "onreadystatechange", "readyState", "responseText", 
				
				"Array", 
				"push", "pop", "sort", "reverse", "shift", "splice", "unshift", "concat", "join", "slice", "indexOf", "lastIndexOf", "length",

				"Boolean",

				// form
				"elements", "onsubmit", "action", 

				"Element",
				"id", "localName", "name", "namespaceURI", 
				"checked", "value", "type", "submit", "select", "src", "click", "options", "selected", "disabled", "rel", // INPUT, IMG, SELECT, LINK, SUBMIT
				"attributes", "getAttribute", "getAttributeNS", "getAttributeNode", "getAttributeNodeNS", "setAttribute", "setAttributeNS", "setAttributeNode", "setAttributeNodeNS", "hasAttribute", "hasAttributeNS", "hasAttributes", "removeAttribute", "removeAttributeNS", "removeAttributeNode", 
				"childNodes", "firstChild","lastChild", "previousSibling", "nextSibling", "parentNode", "ownerDocument", "appendChild", "cloneNode", "hasChildNodes", "insertBefore", "removeChild", "replaceChild", 
				"className", "clientHeight", "clientLeft", "clientTop", "clientWidth", "dir",  "offsetHeight", "offsetLeft", "offsetParent", "offsetTop", "offsetWidth", "scrollHeight", "scrollLeft", "scrollTop", "scrollWidth", 
				"innerHTML", "innerText", "lang", "textContent", 
				"nodeName", "nodeType", "nodeValue", "prefix", "tagName", 
				"addEventListener", "dispatchEvent", "removeEventListener", "attachEvent", "detachEvent", 
				"blur", "focus", "tabIndex", 
				"getElementsByTagName", "getElementsByClassName", "querySelector", "querySelectorAll",
				"item", "normalize", "scrollIntoView", "supports", 
 
				"style", 
				"cssText", "getPropertyValue", "getPropertyCSSValue", "removeProperty", "getPropertyPriority", "setProperty", "item", "parentRule", 
			};
		#endregion
		const string IdRegex = "[A-Za-z0-9_$]+?",
			GroupedIdRegex = "(?<Name>(?!((Object)|(Value)|(SerializedValue)))" + IdRegex + ")";
		static readonly Regex rgxPropertyRelated = new Regex(@"^(((?<Prefix>(get_)|(set_))" + GroupedIdRegex + "(?<Postfix>(Serialized)?))|((?<Prefix>_On)" + GroupedIdRegex + "(?<Postfix>((Changed)|(Changing)))))$");
		static readonly Dictionary<string, string> propertyRelatedSymbols = new Dictionary<string, string>();
		static Dictionary<string, int> KeywordsDic = new Dictionary<string, int>();
		static Dictionary<string, string> BracketsDic = new Dictionary<string, string>(),
			BracketsDicRev = new Dictionary<string, string>();
#if DEBUG
		const string SemiColon = ";\r\n";
#else
		const string SemiColon = ";";
#endif
		static JSObfuscator()
		{
			foreach (string k in JSObfuscator.Keywords)
				JSObfuscator.KeywordsDic[k] = 0;

			BracketsDic["{"] = "}";
			BracketsDic["["] = "]";
			BracketsDic["("] = ")";

			foreach (KeyValuePair<string, string> kv in BracketsDic)
				BracketsDicRev[kv.Value] = kv.Key;

			foreach (string s in new[] { "get_", "set_", "_On", "Changed", "Changing", "Serialized", })
			{
				propertyRelatedSymbols[string.Format("\"{0}\"", s)] = s;
			}
		}
		#endregion
		#region STATIC METHODS
		static bool IsDummyVariable(JSToken token)
		{
			return token.Text.StartsWith("___");
		}
		static bool IsPropertyRelated(JSToken token)
		{
			return rgxPropertyRelated.IsMatch(token.Text);
		}
		static string GetPropertyName(JSToken token)
		{
			return rgxPropertyRelated.Match(token.Text).Groups["Name"].Value;
		}
		#endregion
		#region VARS
		internal Dictionary<string, string> _identifiers = new Dictionary<string, string>(),
			_revIdentifiers = new Dictionary<string, string>();
		string _lastId = null;
		#endregion
		#region METHODS
		#region OBFUSCATION
		private void Obfuscate_CommentWhiteSpace(Context c, bool optimize, StringBuilder sb, JSToken token, JSToken lastToken)
		{
			if (optimize)
			{
				if (lastToken != null)
				{
					this.SkipWhiteSpacesComments(c, 0);
					token = this.Peek(c, 0);
					if (token != null)
					{
						if (token.Type == JSTokenType.BuiltinKeyword ||
							token.Type == JSTokenType.Identifier ||
							token.Type == JSTokenType.Literal)
						{
							if (lastToken.Type == JSTokenType.BuiltinKeyword ||
								lastToken.Type == JSTokenType.Identifier ||
								lastToken.Type == JSTokenType.Literal)
								sb.Append(" ");
							else if (lastToken.Text == "}" && token.Text != "else" && token.Text != "catch" && token.Text != "while")
								sb.Append(";");
						}
					}
				}

				token = lastToken;
			}
			else
			{
				sb.Append(token.Text);
			}
		}
		private void Obfuscate_BuiltinSymbol(Context c, bool optimize, StringBuilder sb, JSToken token)
		{
			sb.Append(token.Text);
		}
		private void Obfuscate_Literal(Context c, bool optimize, StringBuilder sb, JSToken token)
		{
			string s;
			if (propertyRelatedSymbols.TryGetValue(token.Text, out s))
			{
				token.Text = string.Format("\"{0}\"", this.TranslateId(s, optimize));
			}

			sb.Append(token.Text);
		}
		private void Obfuscate_BuiltinKeyword(Context c, bool optimize, StringBuilder sb, JSToken token)
		{
			if (token.Text == "if")
			{
				if (this.Obfuscate_BuiltinKeyword(c, optimize))
					return;
			}
			else if (token.Text == "var")
			{
				if (this.Obfuscate_BuiltinKeyword_Var(c, optimize))
					return;
			}
			sb.Append(token.Text);
		}
		private bool Obfuscate_BuiltinKeyword(Context c, bool optimize)
		{
			this.SkipWhiteSpacesComments(c, 0);
			if (this.Peek(c, 0).Text != "(")
				throw new JSSyntaxException("Expected '(' after 'if' but found '" + this.Peek(c, 0).Text + "'.");

			this.SkipWhiteSpacesComments(c, 1);

			JSToken t = this.Peek(c, 1);

			if (IsDummyVariable(t))
			{
				this.SkipWhiteSpacesComments(c, 2);
				if (this.Peek(c, 2).Text != ")")
					throw new JSSyntaxException("Expected ')' after 'if(___dummy' but found '" + this.Peek(c, 2).Text + "'.");

				this.GetNext(c, 0); this.GetNext(c, 0); this.GetNext(c, 0); // skip '(', '___dummy', ')'

				this.SkipWhiteSpacesComments(c, 0);
				if (this.Peek(c, 0).Text == "{")
				{
					this.GetNext(c, 0); // skip '{'
					this.SkipTokens(c, 0, true, InclusionMode.AllExceptExcluded, new[] { new JSToken(JSTokenType.BuiltinSymbol, "}") });
					this.GetNext(c, 0); // skip '}'
				}
				else
				{
					this.SkipTokens(c, 0, true, InclusionMode.AllExceptExcluded, new[] { new JSToken(JSTokenType.BuiltinSymbol, SemiColon), });
					this.GetNext(c, 0); // skip the ';'
				}
				return true;
			}
			return false;
		}
		private bool Obfuscate_BuiltinKeyword_Var(Context c, bool optimize)
		{
			this.SkipWhiteSpacesComments(c, 1);
			if (this.Peek(c, 1).Type == JSTokenType.Identifier)
			{
				if (IsDummyVariable(this.Peek(c, 1)))
				{
					this.SkipWhiteSpacesComments(c, 2);
					if (this.Peek(c, 2).Text != SemiColon)
						throw new JSSyntaxException("Expected ';' after 'var ___dummy' but found '" + this.Peek(c, 2).Text + "'.");

					for (int i = 0; i < 3; ++i)
						this.GetNext(c, 0);
					return true;
				}
			}
			return false;
		}
		private void Obfuscate_Identifier(Context c, bool optimize, StringBuilder sb, JSToken token)
		{
			Match mProp = rgxPropertyRelated.Match(token.Text);
			if (mProp.Success)
			{
				string obId = string.Format("{0}{1}{2}",
					this.TranslateId(mProp.Groups["Prefix"].Value, optimize),
					this.TranslateId(mProp.Groups["Name"].Value, optimize),
					this.TranslateId(mProp.Groups["Postfix"].Value, optimize));

				sb.Append(obId);
				return;
			}

			if (token.Text == "$PropertyDefinition")
			{
				this.Obfuscate_Identifier_PropertyDefinition(c, optimize);
			}

			sb.Append(this.TranslateId(token.Text, optimize));
		}
		private void Obfuscate_Identifier_PropertyDefinition(Context c, bool optimize)
		{
			if (this.Expect(c, 0, new[] {
				new JSToken(JSTokenType.BuiltinSymbol, "."),
				new JSToken(JSTokenType.Identifier, "Define"),
				new JSToken(JSTokenType.BuiltinSymbol, "("),
				new JSToken(JSTokenType.Identifier, null),
				new JSToken(JSTokenType.BuiltinSymbol, ","),
				new JSToken(JSTokenType.Literal, null),
			}))
			{
				JSToken tokenPropName = this.Peek(c, 5);
				string propName = tokenPropName.Text.Substring(1, tokenPropName.Text.Length - 2);

				tokenPropName.Text = "\"" + this.TranslateId(propName, optimize) + "\"";
			}
		}
		#endregion
		#region OBFUSCATED IDS
		private string PickNextId(string lastId)
		{
			do
			{
				lastId = lastId.PickNext();
			}
			while (JSObfuscator.KeywordsDic.ContainsKey(lastId));
			return lastId;
		}
		/// <summary>
		/// Translates an identifier into its obfuscated counter-part.
		/// </summary>
		/// <param name="identifier"></param>
		/// <param name="optimize">If true, then the ID will be obfuscated if it hasn't been encountered already; if false, it won't be.</param>
		/// <returns></returns>
		private string TranslateId(string identifier, bool optimize)
		{
			if (string.IsNullOrEmpty(identifier)) return "";

			string obId;
			if (!this._identifiers.TryGetValue(identifier, out obId))
			{
				if (optimize || this._revIdentifiers.ContainsKey(identifier)) // if an obfuscated identifier is used elsewhere
				{
					obId = this._lastId = this.PickNextId(this._lastId);
				}
				else
				{
					obId = identifier;
				}
				this._identifiers[identifier] = obId;
				this._revIdentifiers[obId] = identifier;
			}
			return obId;
		}
		#endregion
		#region TOKENS
		void SkipWhiteSpacesComments(Context c, int position)
		{
			this.SkipTokens(c, position, true, InclusionMode.OnlyIncluded, new[] 
			{ 
				new JSToken(JSTokenType.Comment, null),
				new JSToken(JSTokenType.WhiteSpace, null),
			});
		}
		int SkipTokens(Context c, int position, bool remove, InclusionMode mode, JSToken[] tokenTypes)
		{
			Stack<string> brackets = new Stack<string>();
			while (true)
			{
				JSToken next = this.Peek(c, position);
				if (next == null) break;
				if (brackets.Count == 0 && !mode.IsIncluded(tokenTypes, next))
					break;
				else
				{
					if (BracketsDic.ContainsKey(next.Text))
						brackets.Push(next.Text);
					else if (brackets.Count > 0 && BracketsDic[brackets.Peek()] == next.Text)
					{
						brackets.Pop();
					}
					if (remove)
						this.GetNext(c, position);
					else
						++position;
				}
			}
			return position;
		}
		JSToken GetNext(Context c, int position)
		{
			if (c.Queue.Count > position)
			{
				JSToken t = c.Queue[position];
				c.Queue.RemoveAt(position);
				return t;
			}
			return this.ReadToken(c.Reader);
		}
		JSToken Peek(Context c, int position)
		{
			while (c.Queue.Count < position + 1)
				c.Queue.Add(this.ReadToken(c.Reader));
			return c.Queue[position];
		}
		private bool Expect(Context c, int position, JSToken[] tokens)
		{
			for (int i = 0; i < tokens.Length; ++i)
			{
				int pos = position + i;
				this.SkipWhiteSpacesComments(c, pos);
				JSToken token = this.Peek(c, pos);
				if (!object.Equals(token, tokens[i]))
					return false;
			}
			return true;
		}
		#endregion
		#region PARSE
		private JSToken ReadToken(StringReader sr)
		{
			int i = sr.Read();
			if (i == -1) return null;
			char c = (char)i;
			switch (c)
			{
				case '(':
				case ')':
				case '[':
				case ']':
				case '{':
				case '}':
				case '.':
				case ',':
				case '?':
				case ':':
					return new JSToken(JSTokenType.BuiltinSymbol, c.ToString());
				case ';':
					return new JSToken(JSTokenType.BuiltinSymbol, SemiColon);
				case '"':
				case '\'':
					return this.ReadToken_String(sr, c);
				case '/':
					return this.ReadToken_Slash(sr);
				case '+':
				case '-':
				case '&':
				case '|':
				case '=':
				case '<':
				case '>':
					return this.ReadToken_OperatorSingleDouble(sr, c);
				case '*':
				case '%':
				case '!':
				case '^': // XOR
					return new JSToken(JSTokenType.BuiltinSymbol, c.ToString());
				default:
					if (char.IsWhiteSpace(c))
						return this.ReadToken_WhiteSpace(sr, c);
					else if (char.IsNumber(c))
						return this.ReadToken_Number(sr, c);
					else if (char.IsLetter(c) || c == '_' || c == '$')
						return this.ReadToken_Identifier(sr, c);
					else
						return new JSToken(JSTokenType.WhiteSpace, " ");
			}
		}
		private JSToken ReadToken_WhiteSpace(StringReader sr, char c)
		{
			int i;
			StringBuilder sb = new StringBuilder(c.ToString());

			while ((i = sr.Peek()) != -1 && char.IsWhiteSpace(c = (char)i))
			{
				sb.Append(c);
				sr.Read();
			}
			return new JSToken(JSTokenType.WhiteSpace, sb.ToString());
		}
		private JSToken ReadToken_Number(StringReader sr, char c)
		{
			StringBuilder sbNumber = new StringBuilder(c.ToString());
			int i;
			bool point = false, exp = false, hex = false;
			while ((i = sr.Peek()) != -1 && (char.IsNumber(c = char.ToLower((char)i)) || c == '.' || c == 'x' || (c >= 'a' && c <= 'f')))
			{
				switch (c)
				{
					case 'x':
						if (hex)
							throw new JSSyntaxException("Already processed hexadecimal symbol ('x') in the number literal starting with '" + sbNumber + "'.");
						if (!sbNumber.ToString().Equals("0"))
							throw new JSSyntaxException("Expected '0' before 'x' in hexadecimal number.");
						hex = true;
						break;
					case '.':
						if (point)
							throw new JSSyntaxException("Already processed a point in the number literal starting with '" + sbNumber + "'.");

						hex = point = true; // assign both because hex not possible after point
						break;
					case 'e':
						if (exp)
							throw new JSSyntaxException("Already processed exponential sign in the number literal starting with '" + sbNumber + "'.");

						hex = exp = point = true; // assign both because point and hex not possible after exponential
						break;
				}
				if ((c >= 'a' && c <= 'f') && !hex)
					break; // whatever it is, the number stops right here
				sbNumber.Append(c);
				sr.Read();
			}
			return new JSToken(JSTokenType.Literal, sbNumber.ToString());
		}
		private JSToken ReadToken_Identifier(StringReader sr, char c)
		{
			StringBuilder sb = new StringBuilder(c.ToString());

			int i;
			while ((i = sr.Peek()) != -1 && (char.IsLetterOrDigit(c = (char)i) || c == '_' || c == '$'))
			{
				sr.Read();
				sb.Append(c);
			}
			string id = sb.ToString();

			if (JSObfuscator.KeywordsDic.ContainsKey(id))
				return new JSToken(JSTokenType.BuiltinKeyword, id);
			else
				return new JSToken(JSTokenType.Identifier, id);
		}
		private JSToken ReadToken_String(StringReader sr, char c)
		{
			StringBuilder sb = new StringBuilder(c.ToString());
			char terminate = c;
			int i;
			while ((i = sr.Peek()) != -1)
			{
				c = (char)i;
				sr.Read();

				if (c == '\r' || c == '\n')
				{
					throw new JSSyntaxException("Unexpected new-line in string literal starting with '" + sb.ToString() + "'.");
				}
				else if (c == '\\')
				{
					sb.Append(c);

					if ((i = sr.Peek()) == -1)
						throw new JSSyntaxException("Unexpected end of file in the middle of the string literal starting with '" + sb + "'.");
					sr.Read();
					c = (char)i;
					sb.Append(c);
				}
				else if (c == terminate)
				{
					sb.Append(c);
					break;
				}
				else
					sb.Append(c);
			}

			return new JSToken(JSTokenType.Literal, sb.ToString());
		}
		private JSToken ReadToken_Slash(StringReader sr)
		{
			int i;
			char c;
			if ((i = sr.Peek()) == -1)
				throw new JSSyntaxException("Unexpected end of file after a '/'.");
			c = (char)i;
			switch (c)
			{
				case '*': // multi-line comment
					sr.Read();
					StringBuilder sb = new StringBuilder("/*");
					while ((i = sr.Read()) != -1)
					{
						c = (char)i;

						sb.Append(c);
						if (c == '*')
						{
							c = (char)sr.Read();
							sb.Append(c);
							if (c == '/')
								break;
						}
					}
					return new JSToken(JSTokenType.Comment, sb.ToString());
				case '/': // single line comment
					sr.Read();
					while ((i = sr.Peek()) != -1 && ((c = (char)i) != '\r' && c != '\n'))
					{
						sr.Read();
					}
					return new JSToken(JSTokenType.Comment, " ");
				default:
					return new JSToken(JSTokenType.BuiltinSymbol, "/");
			}
		}
		private JSToken ReadToken_OperatorSingleDouble(StringReader sr, char c)
		{
			int i;
			if ((i = sr.Peek()) != -1 && (char)i == c)
			{
				sr.Read();
				return new JSToken(JSTokenType.BuiltinSymbol, c.ToString() + c.ToString());
			}
			else
				return new JSToken(JSTokenType.BuiltinSymbol, c.ToString());
		}
		#endregion
		#endregion
		#endregion
	}
	/// <summary>
	/// Raised when a JS syntax error is encountered.
	/// </summary>
	public class JSSyntaxException : Exception
	{
		/// <summary>
		/// Ctor.
		/// </summary>
		/// <param name="message"></param>
		public JSSyntaxException(string message)
			: base(message)
		{
		}
	}
}
